﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using System.Reflection;

namespace TestMonkey.Models
{

	/// <summary>
	/// Metadata about a class, usually the one being tested
	/// </summary>
	public class ClassModel
	{

		readonly static ConcurrentDictionary<Type, ImmutableList<CodeString>> s_CommonValues = new ConcurrentDictionary<Type, ImmutableList<CodeString>>();

		readonly Setup m_Setup;
		readonly Type m_Type;

		ImmutableList<ConstructorModel> m_Constructors;
		ImmutableList<ClassModel> m_ListT;
		ImmutableList<MethodModel> m_Methods;
		ImmutableList<MethodModel> m_StaticMethods;
		ImmutableList<PropertyModel> m_Properties;
		ImmutableList<MethodModel> m_InstanceMethods;

		/// <summary>
		/// Initializes a new instance of the <see cref="ClassModel"/> class.
		/// </summary>
		/// <param name="setup">The setup for this test generation run.</param>
		/// <param name="type">The type referred to by this class.</param>
		/// <exception cref="System.ArgumentNullException">
		/// setup;setup is null.
		/// or
		/// type;type is null.
		/// </exception>
		public ClassModel(Setup setup, Type type)
		{
			if (setup == null)
				throw new ArgumentNullException("setup", "setup is null.");
			if (type == null)
				throw new ArgumentNullException("type", "type is null.");

			m_Setup = setup;
			m_Type = type;
		}

		/// <summary>
		/// This returns a name that can be used as an identifier.
		/// </summary>
		public CodeString SafeName
		{
			get { return Name.ToString().Replace("<", "").Replace(">", "").Replace("`", ""); }
		}


		/// <summary>
		/// Gets the name of the class.
		/// </summary>
		/// </value>
		public CodeString Name { get { return m_Type.Name; } }

		/// <summary>
		/// Gets the full name scoped to the global namespace.
		/// </summary>
		/// </value>
		public CodeString FullName
		{
			get
			{
				if (m_Type.IsGenericType)
				{
					var typeArgs = from a in m_Type.GetGenericArguments()
								   let c = new ClassModel(m_Setup, a)
								   select c.FullName;

					var name = String.Format("{0}.{1}<{2}>", m_Type.Namespace, m_Type.Name.Substring(0, m_Type.Name.IndexOf("`")), string.Join(", ", typeArgs));
					name = name.Replace("+", ".");
					return "global::" + name;
				}
				else if (m_Type.IsArray)
				{
					return String.Format("global::{0}[{1}]", new ClassModel(m_Setup, m_Type.GetElementType()).FullName, (new String(',', m_Type.GetArrayRank() - 1)));
				}
				else
				{
					return "global::" + m_Type.FullName.Replace("+", ".");
				}
			}
		}

		/// <summary>
		/// Gets a string for generating the default value.
		/// </summary>
		/// <value>
		/// The default value expression such as "defaut(int)".
		/// </value>
		public CodeString DefaultValue
		{
			get { return String.Format("default({0})", FullName); }
		}

		/// <summary>
		/// Gets the namespace for the class.
		/// </summary>
		public CodeString Namespace { get { return m_Type.Namespace; } }

		/// <summary>
		/// Gets a string for creating the object constructor. Used in the helper template.
		/// </summary>
		public CodeString CreateObjectConstructor
		{
			get
			{
				if (!m_Type.IsCreatable())
				{
					var message = "Assert.Inconclusive(\"A custom version of CreateObject is needed for {0}.\");"
						+ Environment.NewLine
						+ "\t\t\treturn default({0});"
						+ Environment.NewLine
						+ @"#warning ""A custom version of CreateObject is needed for {0}.""";

					return string.Format(message, FullName);
				}
				else if (m_Type.HasDefaultConstructor())
					return String.Format("return new {0}();", FullName);
				else
					return String.Format("return {0};", m_Type.GetConstructorCode());
			}
		}



		/// <summary>
		/// Gets a value indicating whether this instance is creatable with the current code generation logic.
		/// </summary>
		/// <value>
		/// <c>true</c> if this instance is creatable; otherwise, <c>false</c>.
		/// </value>
		/// <remarks>A class may have public constructors and still not be creatable by the code generator for a number of reasons.</remarks>
		public bool IsCreatable
		{
			get { return m_Type.IsCreatable(); }
		}

		/// <summary>
		/// Gets the constructor code.
		/// </summary>
		/// <returns></returns>
		/// <exception cref="System.InvalidOperationException">Class is not creatable</exception>
		public CodeString GetConstructorCode()
		{
			if (!m_Type.IsCreatable())
				throw new InvalidOperationException("Class is not creatable");
			else if (m_Type.HasDefaultConstructor())
				return String.Format("new {0}()", FullName);
			else
				return m_Type.GetConstructorCode();
		}


		/// <summary>
		/// This class exposes one or more ways of performing value comparisons (i.e. greater-than or less-than)
		/// </summary>
		public bool IsComparable
		{
			get { return HasInteface("IComparable") || IsIComparableT; }
		}

		/// <summary>
		/// The class exposes IComparable&lt;T&gt; where T is the class
		/// </summary>
		public bool IsIComparableT
		{
			get { return m_Type.GetInterfaces().Any(i => i.Name == "IComparable`1" && i.GetGenericArguments()[0].Name == m_Type.Name); }
		}

		/// <summary>
		/// This class exposes one or more ways of performing value equality
		/// </summary>
		public bool IsEquatable
		{
			get { return IsIEquatableT; }
		}

		/// <summary>
		/// The class exposes IEquatable&lt;T&gt; where T is the class
		/// </summary>
		public bool IsIEquatableT
		{
			get { return m_Type.GetInterfaces().Any(i => i.Name == "IEquatable`1" && i.GetGenericArguments()[0].Name == m_Type.Name); }
		}

		/// <summary>
		/// This indicates the class exposes IEnumerable.
		/// </summary>
		public bool IsIEnumerable
		{
			get { return HasInteface("IEnumerable"); }
		}

		/// <summary>
		/// Determines whether the specified interface name has inteface.
		/// </summary>
		/// <param name="interfaceName">Name of the interface.</param>
		/// <returns></returns>
		public bool HasInteface(string interfaceName)
		{
			return m_Type.GetInterfaces().Any(i => i.Name == interfaceName);
		}

		/// <summary>
		/// Determines whether the specified interface name has inteface.
		/// </summary>
		/// <param name="baseClassName">Name of the base class.</param>
		/// <returns></returns>
		public bool IsSubclassOf(string baseClassName)
		{
			var baseType = m_Type.BaseType;
			while (baseType != null)
			{
				if (baseType.Name == baseClassName)
					return true;
				baseType = m_Type.BaseType;
			}
			return false;
		}

		/// <summary>
		/// Gets the constructors for this class.
		/// </summary>
		/// <value>
		/// The constructors.
		/// </value>
		public ImmutableList<ConstructorModel> Constructors
		{
			get
			{
				if (m_Constructors == null)
				{
					var temp = new List<ConstructorModel>();
					foreach (var constructor in m_Type.GetConstructors())
						temp.Add(new ConstructorModel(constructor));
					m_Constructors = temp.ToImmutableList();
				}
				return m_Constructors;
			}
		}

		/// <summary>
		/// Gets the methods for this class.
		/// </summary>
		/// <value>
		/// The methods.
		/// </value>
		public ImmutableList<MethodModel> Methods
		{
			get
			{
				if (m_Methods == null)
				{
					var temp = new List<MethodModel>();
					foreach (var method in (m_Type.GetMethods()).Where(m => m.DeclaringType == m_Type && !m.IsSpecialName))
						temp.Add(new MethodModel(method));
					m_Methods = temp.ToImmutableList();
				}
				return m_Methods;
			}
		}


		/// <summary>
		/// Gets the instance methods for this class.
		/// </summary>
		/// <value>
		/// The instance methods.
		/// </value>
		public ImmutableList<MethodModel> InstanceMethods
		{
			get
			{
				if (m_InstanceMethods == null)
				{
					var temp = new List<MethodModel>();
					foreach (var method in Methods.Where(m => !m.IsStatic))
						temp.Add(method);
					m_InstanceMethods = temp.ToImmutableList();
				}
				return m_InstanceMethods;
			}
		}

		/// <summary>
		/// Gets the static methods for this class.
		/// </summary>
		/// <value>
		/// The static methods.
		/// </value>
		public ImmutableList<MethodModel> StaticMethods
		{
			get
			{
				if (m_StaticMethods == null)
				{
					var temp = new List<MethodModel>();
					foreach (var method in Methods.Where(m => m.IsStatic))
						temp.Add(method);
					m_StaticMethods = temp.ToImmutableList();
				}
				return m_StaticMethods;
			}
		}


		/// <summary>
		/// Gets the properties for this class.
		/// </summary>
		/// <value>
		/// The properties.
		/// </value>
		public ImmutableList<PropertyModel> Properties
		{
			get
			{
				if (m_Properties == null)
				{
					var shadowingProperties = (from p in m_Type.GetProperties() where IsHidingMember(p) select p).ToList();


					var temp = new List<PropertyModel>();
					foreach (var prop in m_Type.GetProperties())
					{
						if (prop.GetGetMethod() != null && prop.GetGetMethod().IsStatic)
							continue;
						if (prop.GetSetMethod() != null && prop.GetSetMethod().IsStatic)
							continue;


						var isHidden = !shadowingProperties.Contains(prop) && shadowingProperties.Any(p => p.Name == prop.Name);

						//We don't support testing shadowed properties.
						if (isHidden)
							continue;

						var property = new PropertyModel(m_Setup, prop);

						if (m_Setup.ExcludeObsoleteProperties && property.IsObsolete)
							continue;

						if (m_Type.IsSubclassOf(typeof(Attribute)))
						{
							if (prop.Name == "TypeId")
								continue;
						}

						temp.Add(property);
					}
					m_Properties = temp.ToImmutableList();

				}
				return m_Properties;
			}
		}

		/// <summary>
		/// Determines whether a given propery is hiding another property in a base class.
		/// </summary>
		/// <param name="self">The property being checked</param>
		/// <returns></returns>
		static bool IsHidingMember(PropertyInfo self)
		{
			var baseType = self.DeclaringType.BaseType;
			if (baseType == null)
				return false;

			var baseProperty = baseType.GetProperty(self.Name);

			if (baseProperty == null)
				return false;

			if (baseProperty.DeclaringType == self.DeclaringType)
				return false;

			var baseMethodDefinition = baseProperty.GetGetMethod().GetBaseDefinition();
			var thisMethodDefinition = self.GetGetMethod().GetBaseDefinition();

			return baseMethodDefinition.DeclaringType != thisMethodDefinition.DeclaringType;
		}

		/// <summary>
		/// Gets a value indicating whether this instance is nullable.
		/// </summary>
		/// <value>
		/// <c>true</c> if this instance is nullable; otherwise, <c>false</c>.
		/// </value>
		public bool IsNullable
		{
			get { return m_Type.IsClass || m_Type.IsInterface; }
		}

		/// <summary>
		/// Gets a value indicating whether this instance has common values.
		/// </summary>
		/// <value>
		/// <c>true</c> if this instance has common values; otherwise, <c>false</c>.
		/// </value>
		public bool HasCommonValues
		{
			get { return GetCommonValues(m_Type).Count > 0; }
		}

		/// <summary>
		/// Gets the common values for this class.
		/// </summary>
		/// <value>
		/// The common values.
		/// </value>
		public ImmutableList<CodeString> CommonValues
		{
			get { return s_CommonValues.GetOrAdd(m_Type, t => GetCommonValues(t)); }
		}

		/// <summary>
		/// Gets the common values for a given type.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns></returns>
		static ImmutableList<CodeString> GetCommonValues(Type type)
		{
			if (type == typeof(Boolean))
				return GetCommonBooleanValues();

			if (type == typeof(DateTime))
				return GetCommonDateTimeValues(type);

			if (type == typeof(DateTimeOffset))
				return GetCommonDateTimeValues(type);

			if (type.IsNumeric())
				return GetCommonNumericValues(type);

			if (type == typeof(string))
				return GetCommonStringValues();

			return ImmutableList<CodeString>.Empty;
		}

		/// <summary>
		/// Gets the common boolean values.
		/// </summary>
		/// <returns></returns>
		static ImmutableList<CodeString> GetCommonBooleanValues()
		{
			return ImmutableList.Create<CodeString>("true", "false");
		}

		/// <summary>
		/// Gets the common string values.
		/// </summary>
		/// <returns></returns>
		static ImmutableList<CodeString> GetCommonStringValues()
		{
			return ImmutableList.Create<CodeString>("null", @"""""", @"""1""", @"""x""", @""",""", @"""123""", @"""abc""", @"""1,2,3""", @"""a,b,c""", @"""1;2;3""", @"""a;b;c""", @"""http://test.com""", @"""test@test.com""");
		}

		/// <summary>
		/// Gets the common date time values.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns></returns>
		static ImmutableList<CodeString> GetCommonDateTimeValues(Type type)
		{
			var result = new List<CodeString>();

			result.Add(String.Format("global::{0}.MinValue", type.FullName));

			result.Add(String.Format("default({0})", "global::" + type.FullName));
			result.Add(String.Format("{0}.Now", "global::" + type.FullName));
			result.Add(String.Format("{0}.Now.Date", "global::" + type.FullName));

			result.Add(String.Format("global::{0}.MaxValue", type.FullName));


			return result.ToImmutableList();
		}

		/// <summary>
		/// Gets the common numeric values.
		/// </summary>
		/// <param name="type">The type.</param>
		/// <returns></returns>
		static ImmutableList<CodeString> GetCommonNumericValues(Type type)
		{
			var result = new List<CodeString>();
			if (type.IsMinMaxNumeric())
				result.Add(String.Format("global::{0}.MinValue", type.FullName));

			if (type.IsSignedNumeric())
				result.Add(String.Format("({0})(-1)", "global::" + type.FullName));

			result.Add(String.Format("({0})0", "global::" + type.FullName));
			result.Add(String.Format("({0})1", "global::" + type.FullName));

			if (type.IsMinMaxNumeric())
				result.Add(String.Format("global::{0}.MaxValue", type.FullName));


			return result.ToImmutableList();
		}

		/// <summary>
		/// Gets the type this class represents.
		/// </summary>
		/// <value>
		/// The type.
		/// </value>
		internal Type Type
		{
			get { return m_Type; }
		}

		/// <summary>
		/// Gets the types this class can contain by looking for IList&lt;T&gt; interfaces on this class.
		/// </summary>
		public ImmutableList<ClassModel> ListT
		{
			get
			{
				if (m_ListT == null)
				{
					var temp = new List<ClassModel>();

					var interfaces = m_Type.GetInterfaces().Where(i => i.Name == "IList`1");
					foreach (var i in interfaces)
					{
						temp.Add(new ClassModel(m_Setup, i.GetGenericArguments()[0]));
					}

					m_ListT = temp.ToImmutableList();
				}
				return m_ListT;
			}
		}

		/// <summary>
		/// Gets a value indicating whether this instance has methods.
		/// </summary>
		/// <value>
		/// <c>true</c> if this instance has methods; otherwise, <c>false</c>.
		/// </value>
		public bool HasMethods
		{
			get { return StaticMethods.Any() || InstanceMethods.Any(); }
		}
	}
}